package cn.demoncat.util.es.util;

import cn.demoncat.util.lang.LocalDateUtil;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.search.aggregations.AbstractAggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregator.KeyedFilter;
import org.elasticsearch.search.aggregations.bucket.global.GlobalAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval;
import org.elasticsearch.search.aggregations.bucket.histogram.ExtendedBounds;
import org.elasticsearch.search.aggregations.bucket.histogram.HistogramAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.missing.MissingAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.nested.Nested;
import org.elasticsearch.search.aggregations.bucket.nested.NestedAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.range.DateRangeAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.range.GeoDistanceAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.range.IpRangeAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.range.RangeAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.aggregations.metrics.*;

import java.time.LocalDateTime;
import java.util.List;
import java.util.function.Function;

/**
 * ElasticSearch 聚合工具
 *
 * <pre>
 * Metrics 	度量：对数据进行计算，如求和、总计
 * Bucket 	桶：对数据分组，将满足特定条件的数据落在相同的桶中
 * Pipeline	管道：上一个聚合的结果，作为下一个聚合的输入数据
 * 
 * 1：聚合，是查询的附加数据，查询数据还是会返回
 * 2：聚合桶，默认返回10条，按文档count排序
 *
 * </pre>
 * @author 延晓磊
 *
 * @since 2020-9-4
 * 
 * @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/master/search-aggregations.html">官方文档</a>
 * @see <a href="https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/java-aggs.html">Java API</a>
 */
public class AggregationUtil {

	/**
	 * 添加子聚合
	 * 
	 * 1个Query可以有n个Aggregation，1个Aggregation可以有n个Sub Aggregation
	 * 
	 * @param agg	父聚合
	 * @param subs	子聚合：对父聚合（桶）过滤的局部数据进行聚合
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-4
	 */
	public static <T extends AbstractAggregationBuilder<T>>T addSub(T agg, AggregationBuilder ...subs) {
		for (AggregationBuilder sub : subs) {
			agg.subAggregation(sub);
		}
		return agg;
	}

	// ===================================== Metrics 度量 =====================================
	
	/**
	 * 求最小值
	 * 
	 * @param aggName	聚合名
	 * @param field		聚合字段
	 * 
	 * @return Min 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-4
	 */
	public static MinAggregationBuilder min(String aggName, String field) {
		return AggregationBuilders.min(aggName).field(field);
	}
	
	/**
	 * 求最大值
	 * 
	 * @param aggName	聚合名
	 * @param field		聚合字段
	 * 
	 * @return Max 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-4
	 */
	public static MaxAggregationBuilder max(String aggName, String field) {
		return AggregationBuilders.max(aggName).field(field);
	}
	
	/**
	 * 求和
	 * 
	 * @param aggName	聚合名
	 * @param field		聚合字段
	 * 
	 * @return Sum 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-4
	 */
	public static SumAggregationBuilder sum(String aggName, String field) {
		return AggregationBuilders.sum(aggName).field(field);
	}
	
	/**
	 * 求平均值
	 * 
	 * @param aggName	聚合名
	 * @param field		聚合字段
	 * 
	 * @return Avg 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-4
	 */
	public static AvgAggregationBuilder avg(String aggName, String field) {
		return AggregationBuilders.avg(aggName).field(field);
	}
	
	/**
	 * 求总数
	 * 
	 * @param aggName	聚合名
	 * @param field		聚合字段
	 * 
	 * @return ValueCount 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-4
	 */
	public static ValueCountAggregationBuilder count(String aggName, String field) {
		return AggregationBuilders.count(aggName).field(field);
	}
	
	/**
	 * 求去重总数
	 * 
	 * @param aggName
	 * @param field
	 * 
	 * @return Cardinality：COUNT(DISTINCT field)
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-4
	 */
	public static CardinalityAggregationBuilder cardinality(String aggName, String field) {
		return AggregationBuilders.cardinality(aggName).field(field);
	}
	
	/**
	 * 综合统计
	 * 
	 * @param aggName	聚合名
	 * @param field		聚合字段
	 * 
	 * @return Stats：min, max, sum, count, avg
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-4
	 */
	public static StatsAggregationBuilder stats(String aggName, String field) {
		return AggregationBuilders.stats(aggName).field(field);
	}
	
	/**
	 * 求百分位数
	 * 
	 * 统计学术语，用于计算数据的聚焦区间，观察异常数据。
	 * 
	 * 1、将数组（数据列表）排序
	 * 2、将数组切分为100块
	 * 3、百分位，即块编号，如5表示第5个分块，而非5%
	 * 4、百分位数值，即百分位上对应的数据
	 * 
	 * @param aggName		聚合名
	 * @param field			聚合字段
	 * @param percent		百分位（求百分位上的数值），默认[ 1, 5, 25, 50, 75, 95, 99 ]
	 * 	
	 * @return Percentiles：百分位上的数值
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-4
	 */
	public static PercentilesAggregationBuilder percentiles(String aggName, String field, double... percent) {
		 PercentilesAggregationBuilder agg = AggregationBuilders.percentiles(aggName).field(field);
		 return percent == null || percent.length == 0 ? agg : agg.percentiles(percent);
	}
	
	/**
	 * 求百分位
	 * 
	 * @param aggName	聚合名
	 * @param field		聚合字段
	 * @param values	数值（求该数值所处的百分位）
	 * 	
	 * @return PercentileRanks：数值所处的百分位
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-4
	 */
	public static PercentileRanksAggregationBuilder percentileRanks(String aggName, String field, double... values) {
		return AggregationBuilders.percentileRanks(aggName, values).field(field);
	}
	
	/**
	 * 求地理空间的边界
	 * 
	 * @param aggName	聚合名
	 * @param field		聚合字段：GetPoint
	 * 
	 * @return GeoBounds：[左上角GetPoint，右下角GetPoint]
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-4
	 */
	public static GeoBoundsAggregationBuilder geoBounds(String aggName, String field) {
		return AggregationBuilders.geoBounds(aggName).field(field);
	}
	
	/**
	 * 处理父聚合的数据
	 * 
	 * @param top		父聚合
	 * @param aggName	聚合名
	 * @param fn		处理器：对父聚合的命中结果进行 sort排序、from从指定索引开始截断、size指定返回条数
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-4
	 */
	public static <T extends AbstractAggregationBuilder<T>>T topHits(T top, String aggName, Function<TopHitsAggregationBuilder,TopHitsAggregationBuilder> fn) {
		return top.subAggregation(fn.apply(AggregationBuilders.topHits(aggName)));
	}
	
	// ===================================== Bucket 桶 =====================================
	
	/**
	 * 全局桶（单桶）
	 * 
	 * 场景：在查询/聚合数据的同时，聚合全部数据，然后进行比对
	 * 
	 * 1、必须是顶级聚合（作为子聚合没有意义）
	 * 2、忽略搜索中的query条件（针对索引下的所有文档）
	 * 3、通过子聚合对所有文档进行聚合操作
	 * 
	 * @param aggName	聚合名
	 * @param subs		子聚合
	 * 
	 * @return Global 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年9月4日
	 */
	public static GlobalAggregationBuilder global(String aggName, AggregationBuilder... subs) {
		// 创建全局桶：作为顶级聚合存在，通过子聚合对所有文档（忽略搜索中的query条件）进行聚合操作
		GlobalAggregationBuilder global = AggregationBuilders.global(aggName);
		// 添加子聚合
		for (AggregationBuilder sub : subs) {
			global.subAggregation(sub);
		}
		return global;
	}
	
	/**
	 * 过滤桶（单桶）
	 * 
	 * 场景：将聚合处理的数据，减少为1组
	 * 
	 * @param aggName	聚合名
	 * @param filter	过滤查询器：query
	 * 
	 * @return Filter 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年9月4日
	 */
	public static FilterAggregationBuilder filter(String aggName, QueryBuilder filter) {
		return AggregationBuilders.filter(aggName, filter);
	}
	
	/**
	 * 过滤桶（多桶）
	 * 
	 * 场景：将聚合处理的数据，减少为n组
	 * 
	 * @param aggName	聚合名
	 * @param filters	过滤查询器：{桶: {key:query}
	 * 
	 * @return Filters 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年9月4日
	 */
	public static FiltersAggregationBuilder filters(String aggName, KeyedFilter... filters) {
		return AggregationBuilders.filters(aggName, filters);
	}
	
	/**
	 * 字段缺失桶（单桶）
	 * 
	 * 场景：与其它字段桶配置使用，由本桶来存储字段缺失的数据
	 * 
	 * @param aggName	聚合名
	 * @param field		聚合字段：过滤字段缺失或空值的文档
	 * 
	 * @return Missing 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年9月4日
	 */
	public static MissingAggregationBuilder missing(String aggName, String field) {
		return AggregationBuilders.missing(aggName).field(field);
	}

	/**
	 * 词条桶（多桶，相当于GROUP BY）
	 *
	 * @param field		聚合字段/聚合名：必须是keyword等不分词类型，相同值的字段落在一个桶中
	 *
	 * @return	Terms
	 *
	 * @author 延晓磊
	 *
	 * @since 2020年9月4日
	 */
	public static TermsAggregationBuilder terms(String field) {
		return AggregationBuilders.terms(field).field(field);
	}

	/**
	 * 词条桶（多桶，相当于GROUP BY）
	 * <pre>
	 * 扩展设置：
	 * 
	 * 1、设置时区 timeZone
	 * 2、设置格式化 format
	 * 3、设置默认值 missing
	 * 4、最小文档数过滤 minDocCount
	 * 5、数据值过滤includeExclude，支持正则匹配
	 * 6、排序方式 order，支持文档数、字段值、指定子聚合、组合
	 * 7、限制条数 size 默认返回文档数排序的前10条
	 * 
	 * 注意：词条桶不支持多个字段的分组，需要在建索引/存数据时，预先合并字段值到单个分组字段
	 * </pre>
	 * @param aggName	聚合名
	 * @param field		聚合字段：必须是keyword等不分词类型，相同值的字段落在一个桶中
	 * 
	 * @return	Terms 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年9月4日
	 * 
	 * @see <a href="https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/_bucket_aggregations.html#java-aggs-bucket-terms">Java API</a>
	 * @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/7.9/search-aggregations-bucket-terms-aggregation.html">命令说明</a>
	 */
	public static TermsAggregationBuilder terms(String aggName, String field) {
		return AggregationBuilders.terms(aggName).field(field);
	}

	/**
	 * 获取词条桶
	 *
	 * @param agg
	 * @param aggName
	 * @return
	 *
	 * @author 延晓磊
	 * @since 2023年04月06日
	 */
	public static List<? extends Terms.Bucket> getTerms(Aggregations agg, String aggName){
		Terms aggregation = agg.get(aggName);
		return aggregation.getBuckets();
	}

	/**
	 * 范围桶（多桶，范围分组）
	 *
	 * @param field		聚合字段/聚合名：数值类型
	 *
	 * @return Range
	 *
	 * @author 延晓磊
	 *
	 * @since 2020-9-5
	 */
	public static RangeAggregationBuilder range(String field) {
		return AggregationBuilders.range(field).field(field);
	}

	/**
	 * 范围桶（多桶，范围分组）
	 * <pre>
	 * 范围设置：
	 * 
	 * 1、起始 0 ~ 3：addUnboundedTo(3.0 不包含), addUnboundedTo(key, 3.0)   
	 * 2、中间 3 ~ 5：addRange(3.0, 5.0 不包含), addRange(key, 3.0, 5.0)
	 * 3、结尾 5 ~ *：addUnboundedFrom(5.0), addUnboundedFrom(key, 5.0)   
	 * </pre>
	 * @param aggName	聚合名
	 * @param field		聚合字段：数值类型
	 * 
	 * @return Range 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-5
	 */
	public static RangeAggregationBuilder range(String aggName, String field) {
		return AggregationBuilders.range(aggName).field(field);
	}
	
	/**
	 * 时间范围桶（多桶，范围分组）
	 * <pre>
	 * 范围设置：
	 * 
	 * 1、起始：addUnboundedTo(时间戳/时间表达式 不包含), addUnboundedTo(key, 时间戳/时间表达式)   
	 * 2、中间 ：addRange(时间戳/时间表达式, 时间戳/时间表达式 不包含), addRange(key, 时间戳/时间表达式, 时间戳/时间表达式)
	 * 3、结尾 ：addUnboundedFrom(时间戳/时间表达式), addUnboundedFrom(key, 时间戳/时间表达式)   
	 * 4、日期表达式：当前时间 now，加1天 +1d，减1天 -1d，日起始 /d，今天0点 now/d，十个月前1日 now-10M/M
	 * </pre>
	 * @param aggName	聚合名
	 * @param field		聚合字段：日期类型
	 * @param format	输出格式化：format(格式) == 日期 yyyy-MM-dd HH:mm:ss，季节 Q，年的周 w，月的周 W，周的日 e
	 * 
	 * @return Range 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-5
	 * 
	 * @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/7.9/search-aggregations-bucket-daterange-aggregation.html">时间格式</a>
	 * @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/7.9/common-options.html#date-math">时间表达式</a>
	 */
	public static DateRangeAggregationBuilder dateRange(String aggName, String field, String format) {
		return AggregationBuilders.dateRange(aggName).field(field).format(format);
	}
	
	/**
	 * IP范围桶（多桶，范围分组）
	 * <pre>
	 * 范围设置：
	 * 
	 * 1、起始：addUnboundedTo("192.168.1.0" 不包含), addUnboundedTo(key, "192.168.1.0")   
	 * 2、中间：addRange("192.168.1.0", "192.168.2.0" 不包含), addRange(key, "192.168.1.0", "192.168.2.0")
	 * 3、结尾：addUnboundedFrom("192.168.2.0"), addUnboundedFrom(key, "192.168.2.0")   
	 * </pre>
	 * @param aggName	聚合名
	 * @param field		聚合字段：IP类型
	 *
	 * @return Range 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-5
	 */
	public static IpRangeAggregationBuilder ipRange(String aggName, String field) {
		return AggregationBuilders.ipRange(aggName).field(field);
	}
	
	/**
	 * 柱状图（多桶，动态分组）
	 * <pre>
	 * 分组设置：
	 * 
	 * 1、返回方式：keyed(true 自动命名桶并以Map形式返回，false 以Array形式返回)
	 * 2、忽略指定值：missing(忽略的值)
	 * 3、排序方式 order，支持文档数、字段值、指定子聚合、组合 
	 * 4、范围限定：extendedBounds(最小, 最大)，默认为"第1个有数据的桶"到"最后1个有数据的桶"
	 * </pre>
	 * @param aggName	聚合名
	 * @param field		聚合字段：数值类型
	 * @param interval	分组间隔：按interval分桶，数值落在向下取整的桶中
	 * 
	 * @return Histogram 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-5
	 */
	public static HistogramAggregationBuilder histogram(String aggName, String field, double interval) {
		return AggregationBuilders.histogram(aggName).field(field).interval(interval);
	}
	
	/**
	 * 柱状图（多桶，动态分组）
	 * <pre>
	 * 分组设置：
	 * 
	 * 1、返回方式：keyed(true 自动命名桶并以Map形式返回，false 以Array形式返回)
	 * 2、忽略指定值：missing(忽略的值)
	 * 3、排序方式 order，支持文档数、字段值、指定子聚合、组合 
	 * </pre>
	 * @param aggName	聚合名
	 * @param field		聚合字段：数值类型
	 * @param interval	分组间隔：按interval分桶，数值落在向下取整的桶中
	 * @param min		范围最小值：默认为"第1个有数据的桶"
	 * @param max		范围最大值：默认为"最后1个有数据的桶"
	 * 
	 * @return Histogram 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-5
	 */
	public static HistogramAggregationBuilder histogram(String aggName, String field, double interval, double min, double max) {
		return histogram(aggName, field, interval).extendedBounds(min, max);
	}
	
	/**
	 * 日期柱状图（多桶，动态分组）
	 * <pre>
	 * 分组设置：
	 * 
	 * 1、返回方式：keyed(true 自动命名桶并以Map形式返回，false 以Array形式返回)
	 * 2、忽略指定值：missing(忽略的值)
	 * 3、排序方式 order，支持文档数、字段值、指定子聚合、组合 
	 * 4、范围限定：extendedBounds(最小, 最大)，默认为"第1个有数据的桶"到"最后1个有数据的桶"；因为ES保存为ETC时区问题，用timeStr方式设置时多8小时，建议在程序中对返回数据的分组做增补
	 * </pre>
	 * @param aggName	聚合名
	 * @param field		聚合字段：日期类型
	 * @param format	输出格式化：format(格式) == 日期 yyyy-MM-dd HH:mm:ss，季节 Q，年的周 w，月的周 W，周的日 e
	 * @param interval	分组间隔：按interval分桶，日期落在向下取整的桶中
	 * 
	 * @return Histogram 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-5
	 */
	public static DateHistogramAggregationBuilder dateHistogram(String aggName, String field, String format, DateHistogramInterval interval) {
		return AggregationBuilders.dateHistogram(aggName).field(field).calendarInterval(interval).format(format);
	}
	
	/**
	 * 日期柱状图（多桶，动态分组）
	 * <pre>
	 * 分组设置：
	 * 
	 * 1、返回方式：keyed(true 自动命名桶并以Map形式返回，false 以Array形式返回)
	 * 2、忽略指定值：missing(忽略的值)
	 * 3、排序方式 order，支持文档数、字段值、指定子聚合、组合 
	 * </pre>
	 * @param aggName	聚合名
	 * @param field		聚合字段：日期类型
	 * @param format	输出格式化：format(格式) == 日期 yyyy-MM-dd HH:mm:ss，季节 Q，年的周 w，月的周 W，周的日 e
	 * @param interval	分组间隔：按interval分桶，日期落在向下取整的桶中
	 * @param startTime	起始时间：默认为"第1个有数据的桶"，结束桶为当前
	 * 
	 * @return Histogram 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-5
	 */
	public static DateHistogramAggregationBuilder dateHistogram(String aggName, String field, String format, DateHistogramInterval interval, LocalDateTime startTime) {
		// 转换为UTC时间戳（解决ES时区问题）
		return dateHistogram(aggName, field, format, interval).extendedBounds(new ExtendedBounds(LocalDateUtil.getUtcTime(startTime), LocalDateUtil.getUtcTime(LocalDateTime.now())));
	}
	
	/**
	 * 位置距离桶（多桶，距离分组）
	 * <pre>
	 * 距离设置：
	 * 
	 * 1、起始 0 ~ 3：addUnboundedTo(3.0 不包含), addUnboundedTo(key, 3.0)   
	 * 2、中间 3 ~ 5：addRange(3.0, 5.0 不包含), addRange(key, 3.0, 5.0)
	 * 3、结尾 5 ~ *：addUnboundedFrom(5.0), addUnboundedFrom(key, 5.0)   
	 * </pre>
	 * @param aggName	聚合名
	 * @param field		聚合字段：数值类型
	 * @param point		基准位置
	 * @param unit		距离单位
	 *
	 * @return Range 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020-9-5
	 */
	public static GeoDistanceAggregationBuilder geoDistance(String aggName, String field, GeoPoint point, DistanceUnit unit) {
		return AggregationBuilders.geoDistance(aggName, point).field(field).unit(unit);
	}

	/**
	 * 嵌套聚合
	 *
	 * @param field		字段名/聚合名
	 * @param subs		子聚合，字段为 field.subField
	 * @return
	 *
	 * @author 延晓磊
	 * @since 2023年04月06日
	 */
	public static NestedAggregationBuilder nested(String field, AggregationBuilder ... subs){
		return nested(field, field, subs);
	}

	/**
	 * 嵌套聚合
	 *
	 * @param aggName	聚合名
	 * @param field		字段名
	 * @param subs		子聚合，字段为 field.subField
	 * @return
	 *
	 * @author 延晓磊
	 * @since 2023年04月06日
	 */
	public static NestedAggregationBuilder nested(String aggName, String field, AggregationBuilder ... subs){
		return addSub(AggregationBuilders.nested(aggName, field), subs);
	}

	/**
	 * 获取嵌套桶
	 *
	 * @param agg
	 * @param aggName
	 * @return
	 *
	 * @author 延晓磊
	 * @since 2023年04月06日
	 */
	public static Aggregations getNested(Aggregations agg, String aggName){
		Nested nested =  agg.get(aggName);
		return nested.getAggregations();
	}
}
